#ifndef HYPERCUBE_H
#define HYPERCUBE_H

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <cmath>
#include <algorithm>
#include <map>
#include "Clustering.h"
#include "DataImage.h"
#include "hyperspace.h"

using namespace std;

class HyperSpace;

class HyperCube{
    private:
        const unsigned dimensions;
        const double edge_length;
        string hypercube_key;   
        vector< DATA_ENTITY_CLASS_NAME > objects;  
        vector< string > neighbors;   
        vector< unsigned int > entities_sum;  

    public:
        HyperCube( unsigned dimensions, const double *upper_bounds, double edge_length) : dimensions(dimensions), edge_length(edge_length) {
            this->hypercube_key = HyperCube::getKeyFromArray( upper_bounds, dimensions, edge_length );
            
            for(unsigned i=0 ; i < this->dimensions ; i++){
                this->entities_sum.push_back(0);
            }
        }

        HyperCube( const HyperCube& other ) : dimensions(other.dimensions), edge_length(other.edge_length) {
            
            this->hypercube_key = other.hypercube_key;
            
            this->objects.clear();
            for(unsigned i=0 ; i < other.objects.size() ; i++){
                this->objects.push_back(other.objects[i]);
            }
            
            this->entities_sum.clear();
            for(unsigned i=0; i < other.entities_sum.size() ; i++){
                this->entities_sum.push_back( other.entities_sum[i] );
            }
            
            this->neighbors.clear();
            for(unsigned i=0 ; i < other.neighbors.size() ; i++){
                this->neighbors.push_back(other.neighbors[i]);
            }
        }

        ~HyperCube(){
            this->objects.clear();
            this->neighbors.clear();
        }
        unsigned numObjects() const {  return this->objects.size();  }
        
        friend ostream& operator<<(ostream& os, const HyperCube& hc){
            cout << "-------- HyperCube ---------" << endl;
            cout << "Dimensions: " << hc.dimensions << endl;
            cout << "Edge lenght: " << hc.edge_length << endl;
            cout << "Key: (" << hc.hypercube_key << ')' << endl;
            cout << "# of entities: " << hc.numObjects() << endl;
            cout << "Entities sum: (";
            for(unsigned i=0 ; i < hc.entities_sum.size() ; i++){
                if( i != 0 )  cout << ',';
                cout << hc.entities_sum[i];
            }
            cout << ")" << endl;
            cout << "Neighbors: ";
            for(vector<string>::const_iterator it = hc.neighbors.begin() ; it != hc.neighbors.end() ; it++){
                if( it != hc.neighbors.begin() )  cout << " , ";
                cout << *it;
            }
            cout << endl;
            cout << "-------- end ---------" << endl;
            return os;
        }

        void addObject( const DATA_ENTITY_CLASS_NAME& object ){
            double *upper_bounds = HyperCube::getArrayFromKey(this->hypercube_key, this->dimensions);
            double lower_bounds[this->dimensions];
            
            bool outside_hypercube = false;
            for(unsigned i=0 ; i < this->dimensions ; i++){
                
                lower_bounds[i] = upper_bounds[i] - this->edge_length;
                unsigned int curr_comp_value = object.getComponentValue(i); 
                if( (curr_comp_value < lower_bounds[i]) || (curr_comp_value >= upper_bounds[i]) ){
                    
                    cerr << "Component " << i << " should be in [" << lower_bounds[i];
                    cerr << "," << upper_bounds[i] << ")" << endl;
                    outside_hypercube = true;
                    break;
                }
            }
            
            delete[] upper_bounds;
            if( !outside_hypercube ){
                this->objects.push_back(object);  
                
                for(unsigned i=0 ; i < this->dimensions; i++){
                    this->entities_sum[i] += object.getComponentValue(i);
                }
            }
            return;
        }

        vector<DATA_ENTITY_CLASS_NAME>& retrieveObjects(){
            return this->objects;
        }

        void setNeighbors( const vector<string>& neighbors ){
            
            vector<string>::const_iterator it = neighbors.begin();
            while( it != neighbors.end() ){
                bool isntSameHyperCube = ( *it != this->hypercube_key );
                if( isntSameHyperCube ){
                    this->neighbors.push_back(*it);
                }
                it++;
            }
            return;
        }

        const vector<string>& getNeighbors(){
            return this->neighbors;
        }

        static string getKeyFromArray( const double *upper_bounds, unsigned dimension , double edge_length) {
            ostringstream str_buf;
            for(unsigned i=0 ; i < dimension ; i++){
                double curr_index =  upper_bounds[i] ;
                if( i != 0 )  str_buf << ',';
                str_buf << curr_index;
            }
            return str_buf.str();
        }

        static double * getArrayFromKey( const string& str_key, unsigned dimension ){
            double *upp_bounds = new double[dimension];
            istringstream string_input(str_key);   
            stringstream string_output;
            unsigned i = 0;
            
            while( !string_input.eof() && (i < dimension) ){
                char curr_char = (char) string_input.get();
                
                if( (curr_char == ',') || string_input.eof() ){
                    string str;
                    string_output >> str;
                    upp_bounds[i] = atof(str.c_str());  
                    i++;  
                    string_output.clear();   
                }

                else{  
                    string_output << curr_char;
                }
            } 
            return upp_bounds;
        }

        bool isEmpty() const {  return (this->numObjects() == 0 );   }

        void removeEmptyNeighbors( const vector<string>& empty_neighbors ){
            
            vector<string>::iterator it = this->neighbors.begin();
            while( it != this->neighbors.end() ){
                
                vector<string>::const_iterator wanted_key = find(
                        empty_neighbors.begin() , empty_neighbors.end() , *it );
                if( wanted_key != empty_neighbors.end() ){  
                    it = this->neighbors.erase(it); 
                    continue;
                }
                it++;
            }
        }

        bool isNeighbor( const std::vector<std::string>& hypercube_keys ,  const map< string, HyperCube>& cubes) const {
            bool is_neighbor = false;
            vector<string>::const_iterator it = hypercube_keys.begin();
            for( ; it != hypercube_keys.end() ; it++){
                
                vector<string>::const_iterator wanted_key = find( this->neighbors.begin(), this->neighbors.end(), *it);
                bool found_it = (wanted_key != this->neighbors.end() );
                
                if( found_it ){
                    
                    DATA_ENTITY_CLASS_NAME this_mean = this->getMeanElement();
                    const HyperCube& neighbor_cube = cubes.find(*wanted_key)->second;
                    DATA_ENTITY_CLASS_NAME neighbor_mean = neighbor_cube.getMeanElement();
                    double distance =  this_mean.euclidicDistance(&neighbor_mean);
                    
                    is_neighbor = ( distance <= (2*this->edge_length) );
                    if( is_neighbor ) break;
                }
            }
            return is_neighbor;
        }

        DATA_ENTITY_CLASS_NAME getMeanElement() const{
            DATA_ENTITY_CLASS_NAME mean;
            const unsigned num_entities = this->numObjects();
            
            ostringstream mean_str;
            vector<unsigned int>::const_iterator it = this->entities_sum.begin();
            for( ; it != this->entities_sum.end() ; it++){
                double curr_component_mean = *it / (num_entities*1.0);
                if( it != this->entities_sum.begin() )  mean_str << ',';
                mean_str << curr_component_mean;
            }
            mean.entityFromString( mean_str.str() );
            return mean;
        }

};  


#endif

